package net.cassite.pure.ioc;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import net.cassite.pure.ioc.annotations.Singleton;
import net.cassite.pure.ioc.handlers.*;
import net.cassite.pure.ioc.handlers.constructor.ConstructorDefaultFilter;
import net.cassite.pure.ioc.handlers.constructor.DefaultConstructorFilter;
import net.cassite.pure.ioc.handlers.param.*;
import net.cassite.pure.ioc.handlers.type.*;

import static net.cassite.style.Style.*;
import static net.cassite.style.aggregation.Aggregation.*;
import static net.cassite.style.reflect.Reflect.*;

import net.cassite.style.reflect.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The class is to control the process of handling annotations and constructing
 * objects. <br>
 * Annotation handlers are divided into 4 kinds. <br>
 * <ul>
 * <li>TypeAnnotationHandler : enabled when constructing objects</li>
 * <li>ConstructorFilter : enabled when selecting constructors</li>
 * <li>ParamAnnotationHandler : enabled when getting parameter values of a
 * constructor</li>
 * <li>SetterAnnotationHandler : enabled when invoking a setter. <br>
 * (field, method, parameter annotation of a setter are considered as 'setter
 * annotation'.)</li>
 * </ul>
 * All handlers process in a simple logic: <br>
 * enable handlers in a chain, low priority handler are earlier to be called,
 * <br>
 * handlers are recommended to call higher priority handlers, check return <br>
 * value, then decide whether to run its own handling process.
 *
 * @author wkgcass
 * @see net.cassite.pure.ioc.handlers.TypeHandlerChain
 * @see net.cassite.pure.ioc.handlers.TypeAnnotationHandler
 * @see net.cassite.pure.ioc.handlers.type.TypeDefaultHandler
 * @see net.cassite.pure.ioc.handlers.ConstructorFilterChain
 * @see net.cassite.pure.ioc.handlers.ConstructorFilter
 * @see net.cassite.pure.ioc.handlers.ParamHandlerChain
 * @see net.cassite.pure.ioc.handlers.ParamAnnotationHandler
 */
public abstract class IOCController {

        private static final Logger LOGGER = LoggerFactory.getLogger(IOCController.class);

        // ===========================
        // ==========handlers=========
        // ===========================

        /**
         * param annotation handlers
         */
        private static List<ParamAnnotationHandler> paramAnnotationHandlers = new ArrayList<>();
        /**
         * constructor filters
         */
        private static List<ConstructorFilter> constructorFilters = new ArrayList<>();
        /**
         * type annotation handler
         */
        private static List<TypeAnnotationHandler> typeAnnotationHandlers = new ArrayList<>();

        // ============================
        // =========container==========
        // ============================
        /**
         * singleton class instances<br>
         * classes with Singleton annotation
         *
         * @see Singleton
         */
        private static Map<Class<?>, Object> singletons = new ConcurrentHashMap<>();
        public static final Scope rootScope = new Scope();

        static {
                rootScope.bind(Scope.class, scope -> scope);
        }

        protected IOCController() {
        }

        /**
         * get instance by class. <br>
         * this method would check TypeHandlerChain to generate instance.
         *
         * @param scope          wire scope
         * @param cls            class
         * @param expectingClass the class originally wanted
         * @return instance handled by type chain
         */
        protected static Object get(Scope scope, Class<?> cls, Class<?> expectingClass) {
                LOGGER.debug("Invoking get(Class) to get instance of {}", cls);
                TypeHandlerChain chain = new TypeHandlerChain(typeAnnotationHandlers, cls.getAnnotations());
                return chain.next().handle(scope, cls, expectingClass, chain);
        }

        @SuppressWarnings("unchecked")
        protected static void fillField(Scope scope, Object target, @SuppressWarnings("rawtypes") FieldSupport f) {
                Annotation[] annotations = f.getMember().getAnnotations();
                LOGGER.debug("Wiring object {}'s field {} with annotations {}", target, f, annotations);

                ParamHandlerChain chain = new ParamHandlerChain(paramAnnotationHandlers, annotations);
                Object obj = chain.next().handle(scope, f, f.getMember().getType(), f.getMember().getType(), annotations, chain);
                f.set(target, obj);
        }

        /**
         * invoke a setter<br>
         * This method would check corresponding field(might not found, that's
         * unimportant) and method annotations, then go through
         * SetterHandlerChain.
         *
         * @param scope  scope
         * @param target object to invoke
         * @param m      setter
         */
        @SuppressWarnings("unchecked")
        protected static void invokeSetter(Scope scope, Object target, @SuppressWarnings("rawtypes") MethodSupport m) {
                LOGGER.debug("Wiring object {}'s method {}", target, m);

                List<FieldSupport<?, Object>> fields = cls(target).allFields();

                Set<Annotation> annset = new HashSet<>();

                // get inferred field name
                String fieldName = m.name().substring(3);

                // try to get field and its annotations ( ignore field name case)
                If($(fields).findOne(f -> f.name().equalsIgnoreCase(fieldName)), found -> {
                        Collections.addAll(annset, found.getMember().getAnnotations());
                }).End();

                // try to get method annotations
                Collections.addAll(annset, m.getMember().getAnnotations());
                // parameter value annotations
                Collections.addAll(annset, m.getMember().getParameterAnnotations()[0]);

                LOGGER.debug("With Annotations: {}", annset);

                // handle
                Annotation[] annoArray = annset.toArray(new Annotation[annset.size()]);
                ParamHandlerChain chain = new ParamHandlerChain(paramAnnotationHandlers, annoArray);
                try {
                        Object o = chain.next().handle(scope, m, m.argTypes()[0], m.argTypes()[0], annoArray, chain);
                        m.invoke(target, o);
                } catch (IrrelevantAnnotationHandlingException | IgnoredAnnotationHandlingException ignore) {
                }
        }

        /**
         * construct with given class and parameter values.
         *
         * @param con             constructor to call
         * @param parameterValues parameter values
         * @return new instance generated by the constructor and parameterValues
         */
        private static Object construct(ConstructorSup<?> con, Object[] parameterValues) {
                return con.newInstance(parameterValues);
        }

        /**
         * construct with given class <br>
         * this method gets constructor from constructor filter chain, and then
         * get parameters from ParamHandlerChain. <br>
         * finally call {@link #construct(ConstructorSup, Object[])}
         *
         * @param scope wire scope
         * @param cls   class to construct
         * @return instance of the class
         * @see ParamHandlerChain
         */
        protected static Object constructObject(Scope scope, @SuppressWarnings("rawtypes") Class cls) {
                LOGGER.debug("Invoking constructObject(Class) to get instance of type {}", cls);
                Set<Annotation> set = new HashSet<>();
                for (Constructor<?> cons : cls.getConstructors()) {
                        Collections.addAll(set, cons.getAnnotations());
                }
                LOGGER.debug("--gathered annotations are {}", set);
                ConstructorFilterChain chain = new ConstructorFilterChain(constructorFilters, set);
                @SuppressWarnings("unchecked")
                ConstructorSup<?> con = chain.next().handle(scope, cls(cls).constructors(), chain);

                LOGGER.debug("--retrieved constructor is {}", con);

                Object[] pv = new Object[con.argCount()];
                for (int i = 0; i < pv.length; ++i) {
                        ParamHandlerChain chain2 = new ParamHandlerChain(paramAnnotationHandlers, con.getMember().getParameterAnnotations()[i]);
                        pv[i] = chain2.next().handle(scope, con, con.argTypes()[i], con.argTypes()[i], con.getMember().getParameterAnnotations()[i], chain2);
                        LOGGER.debug("--parameter at index {} is {}", i, pv[i]);
                }
                return construct(con, pv);
        }

        /**
         * get instance with class. <br>
         * this method would check whether it IsSingleton <br>
         * then invoke constructObject(Class&lt;?&gt;) to get instance<br>
         * <br>
         * This method might call singletons.get directly if the singleton class
         * already in or finished construction <b>or</b>
         * {@link #constructObject(Scope, Class)}<br>
         * <b>Note</b> that, this method will construct the given class, it will
         * not go through TypeHandlerChain, e.g. it won't find out whether it's
         * redirected to another class(using Default annotation)<br>
         * Use {@link #get(Scope, Class, Class)} to do the type check.
         *
         * @param scope wire session
         * @param cls   class to instantiate
         * @return instance of the class.
         * @see #constructObject(Scope, Class)
         * @see #get(Scope, Class, Class)
         */
        @SuppressWarnings("unchecked")
        protected static Object getObject(Scope scope, @SuppressWarnings("rawtypes") Class cls) {
                LOGGER.debug("Invoking getObject(Class) to get instance of type {}", cls);
                if (cls.isAnnotationPresent(Singleton.class)) {
                        LOGGER.debug("--is singleton");
                        if (singletons.containsKey(cls)) {
                                return singletons.get(cls);
                        } else {
                                return constructObject(scope, cls);
                        }
                } else {
                        return constructObject(scope, cls);
                }
        }

        /**
         * This method invokes given method with inferred arguments<br>
         * It will check the ParamHandlerChain, and retrieve arguments.
         *
         * @param scope  wire scope
         * @param method method to invoke
         * @param target object of the method to invoke on
         * @return invocation result
         * @see ParamHandlerChain
         */
        @SuppressWarnings({"unchecked"})
        protected static Object invokeMethod(Scope scope, @SuppressWarnings("rawtypes") MethodSupport method, Object target) {
                LOGGER.debug("Invoking method {} of object {}", method, target);

                Object[] pv = new Object[method.argCount()];
                for (int i = 0; i < pv.length; ++i) {
                        ParamHandlerChain chain2 = new ParamHandlerChain(paramAnnotationHandlers, method.getMember().getParameterAnnotations()[i]);
                        pv[i] = chain2.next().handle(scope, method, method.argTypes()[i], method.argTypes()[i], method.getMember().getParameterAnnotations()[i], chain2);
                }
                return method.invoke(target, pv);
        }

        // ================================
        // ========register/retrieve=======
        // ================================

        public static void register(ConstructorFilter ah) {
                LOGGER.info("registering {}", ah);
                constructorFilters.add(ah);
        }

        public static void register(ParamAnnotationHandler ah) {
                LOGGER.info("registering {}", ah);
                paramAnnotationHandlers.add(ah);
        }

        public static void register(TypeAnnotationHandler ah) {
                LOGGER.info("registering {}", ah);
                typeAnnotationHandlers.add(ah);
        }

        static void registerSingleton(Object instance) {
                if (singletons.containsKey(instance.getClass())) {
                        throw new ConstructingMultiSingletonException(instance.getClass());
                }
                if (null != instance.getClass().getAnnotation(Singleton.class)) {
                        singletons.put(instance.getClass(), instance);
                }
        }

        /**
         * Automatically register built-in handlers
         */
        public static void autoRegister() {
                LOGGER.info("start auto registering...");
                register(new DefaultConstructorFilter());
                register(new ConstructorDefaultFilter());

                register(new ParamScopeHandler());
                register(new PrimitiveParameterHandler());
                register(new DefaultParamHandler());
                register(new ParamUseHandler());
                register(new ParamExtendHandler());
                register(new ParamForceHandler());
                register(new ParamIgnoreHandler());

                register(new TypeAOPHandler());
                register(new TypeWireHandler());
                register(new DefaultTypeHandler());
                register(new TypeIsSingletonHandler());
                register(new TypeDefaultHandler());
                register(new TypeExtendHandler());
        }

        public static void closeRegistering() {
                paramAnnotationHandlers = readOnly(paramAnnotationHandlers);
                constructorFilters = readOnly(constructorFilters);
                typeAnnotationHandlers = readOnly(typeAnnotationHandlers);
                LOGGER.info("registration closed.");
        }

        /**
         * determine whether there are handlers registered
         *
         * @return true if handlers had been registered, false otherwise
         */
        static boolean isWithHandlers() {
                return !typeAnnotationHandlers.isEmpty() || !constructorFilters.isEmpty() || !paramAnnotationHandlers.isEmpty();
        }
}
